// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/web/navigation/session_storage_builder.h"

#include "base/strings/sys_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "ios/web/common/features.h"
#import "ios/web/navigation/wk_navigation_util.h"
#import "ios/web/public/session/crw_navigation_item_storage.h"
#import "ios/web/public/session/crw_session_storage.h"
#include "ios/web/public/test/web_test.h"
#import "ios/web/test/fakes/crw_fake_back_forward_list.h"
#import "ios/web/web_state/ui/crw_web_view_navigation_proxy.h"
#import "ios/web/web_state/web_state_impl.h"
#import "third_party/ocmock/OCMock/OCMock.h"

#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif

namespace web {

using wk_navigation_util::kMaxSessionSize;

// WebState that provides Mock CRWWebViewNavigationProxy object.
class WebStateWithMockProxy : public WebStateImpl {
 public:
  explicit WebStateWithMockProxy(const CreateParams& params)
      : WebStateImpl(params),
        mock_web_view_(OCMProtocolMock(@protocol(CRWWebViewNavigationProxy))),
        fake_wk_list_([[CRWFakeBackForwardList alloc] init]) {
    OCMStub([mock_web_view_ backForwardList]).andReturn(fake_wk_list_);
  }

  id<CRWWebViewNavigationProxy> mock_web_view() { return mock_web_view_; }

  CRWFakeBackForwardList* fake_wk_list() { return fake_wk_list_; }

 private:
  id<CRWWebViewNavigationProxy> GetWebViewNavigationProxy() const override {
    return mock_web_view_;
  }

  id mock_web_view_ = nil;
  CRWFakeBackForwardList* fake_wk_list_ = nil;
};

using SessionStorageBuilderTest = WebTest;

void SetNavigationItemSizedStrings(WebStateWithMockProxy& web_state,
                                   int index,
                                   int url_length,
                                   int virtual_url_length,
                                   int title_length,
                                   int referrer_url_length) {
  NavigationItemImpl* item =
      web_state.GetNavigationManagerImpl().GetNavigationItemImplAtIndex(index);
  if (url_length) {
    NSString* url = [@"https://" stringByPaddingToLength:url_length
                                              withString:@"a"
                                         startingAtIndex:0];
    item->SetURL(GURL(base::SysNSStringToUTF8(url)));
  } else {
    item->SetURL(GURL());
  }
  if (virtual_url_length) {
    NSString* virtual_url =
        [@"https://" stringByPaddingToLength:virtual_url_length
                                  withString:@"b"
                             startingAtIndex:0];
    item->SetVirtualURL(GURL(base::SysNSStringToUTF8(virtual_url)));
  } else {
    item->SetVirtualURL(GURL());
  }
  if (title_length) {
    NSString* title = [@"" stringByPaddingToLength:title_length
                                        withString:@"c"
                                   startingAtIndex:0];
    item->SetTitle(base::SysNSStringToUTF16(title));
  } else {
    item->SetTitle(base::SysNSStringToUTF16(@""));
  }

  Referrer referrer;
  if (referrer_url_length) {
    NSString* referrer_url =
        [@"https://" stringByPaddingToLength:referrer_url_length
                                  withString:@"d"
                             startingAtIndex:0];
    referrer.url = GURL(base::SysNSStringToUTF8(referrer_url));
  }
  item->SetReferrer(referrer);
}

// Tests building storage for session that is longer than kMaxSessionSize with
// last committed item at the end of the session.
TEST_F(SessionStorageBuilderTest, BuildStorageForExtraLongSession) {
  // Create WebState with navigation item count that exceeds kMaxSessionSize.
  WebState::CreateParams params(GetBrowserState());
  WebStateWithMockProxy web_state(params);

  NSMutableArray* back_urls = [NSMutableArray array];
  for (int i = 0; i < kMaxSessionSize; i++) {
    [back_urls addObject:[NSString stringWithFormat:@"http://%d.test", i]];
  }
  NSString* const current_url = @"http://current.test";
  [web_state.fake_wk_list() setCurrentURL:current_url
                             backListURLs:back_urls
                          forwardListURLs:nil];
  OCMStub([web_state.mock_web_view() URL])
      .andReturn([NSURL URLWithString:current_url]);
  NavigationManager* navigation_manager = web_state.GetNavigationManager();
  int original_item_count = navigation_manager->GetItemCount();
  ASSERT_EQ(kMaxSessionSize + 1, original_item_count);

  // Verify that storage item count does not exceed kMaxSessionSize.
  SessionStorageBuilder builder;
  CRWSessionStorage* storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  int stored_item_count = storage.itemStorages.count;
  ASSERT_EQ(kMaxSessionSize, stored_item_count);

  // Walk backwards and verify that URLs in the storage match original URLs.
  for (int i = 0; i < kMaxSessionSize; i++) {
    NavigationItem* item =
        navigation_manager->GetItemAtIndex(original_item_count - i - 1);
    CRWNavigationItemStorage* item_storage =
        [storage.itemStorages objectAtIndex:stored_item_count - i - 1];
    EXPECT_EQ(item->GetURL(), item_storage.URL) << "index: " << i;
  }
}

// Tests building storage for session that has items with
// ShouldSkipSerialization flag. The session length after skipping the items is
// not longer than kMaxSessionSize.
TEST_F(SessionStorageBuilderTest, ShouldSkipSerializationItems) {
  // Create WebState with navigation item count that exceeds kMaxSessionSize.
  WebState::CreateParams params(GetBrowserState());
  WebStateWithMockProxy web_state(params);

  NSMutableArray* back_urls = [NSMutableArray array];
  for (int i = 0; i < kMaxSessionSize; i++) {
    [back_urls addObject:[NSString stringWithFormat:@"http://%d.test", i]];
  }
  NSString* const current_url = @"http://current.test";
  [web_state.fake_wk_list() setCurrentURL:current_url
                             backListURLs:back_urls
                          forwardListURLs:nil];
  OCMStub([web_state.mock_web_view() URL])
      .andReturn([NSURL URLWithString:current_url]);
  NavigationManager* navigation_manager = web_state.GetNavigationManager();
  int original_item_count = navigation_manager->GetItemCount();
  ASSERT_EQ(kMaxSessionSize + 1, original_item_count);

  const int kSkippedItemIndex = kMaxSessionSize - 1;
  web_state.GetNavigationManagerImpl()
      .GetNavigationItemImplAtIndex(kSkippedItemIndex)
      ->SetShouldSkipSerialization(true);

  // Verify that storage item count does not exceed kMaxSessionSize.
  SessionStorageBuilder builder;
  CRWSessionStorage* storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  int stored_item_count = storage.itemStorages.count;
  ASSERT_EQ(kMaxSessionSize, stored_item_count);

  // Verify that URLs in the storage match original URLs without skipped item.
  for (int storage_index = 0, item_index = 0;
       storage_index < kMaxSessionSize &&
       item_index < web_state.GetNavigationManagerImpl().GetItemCount();
       storage_index++, item_index++) {
    if (item_index == kSkippedItemIndex) {
      item_index++;
    }
    NavigationItem* item = navigation_manager->GetItemAtIndex(item_index);

    CRWNavigationItemStorage* item_storage =
        [storage.itemStorages objectAtIndex:storage_index];
    EXPECT_EQ(item->GetURL(), item_storage.URL) << "item_index: " << item_index;
  }
}

// Tests building storage for session that has URL longer than
// url::kMaxURLChars.
TEST_F(SessionStorageBuilderTest, SkipLongUrls) {
  // Create WebState with navigation item count that exceeds kMaxSessionSize.
  WebState::CreateParams params(GetBrowserState());
  WebStateWithMockProxy web_state(params);

  NSString* long_url =
      [@"https://" stringByPaddingToLength:url::kMaxURLChars + 1
                                withString:@"a"
                           startingAtIndex:0];
  NSString* normal_url = @"https://foo.test";
  NSString* const current_url = normal_url;
  [web_state.fake_wk_list() setCurrentURL:normal_url
                             backListURLs:@[ long_url ]
                          forwardListURLs:nil];
  OCMStub([web_state.mock_web_view() URL])
      .andReturn([NSURL URLWithString:current_url]);
  NavigationManager* navigation_manager = web_state.GetNavigationManager();
  ASSERT_EQ(2, navigation_manager->GetItemCount());

  web_state.GetNavigationManagerImpl()
      .GetNavigationItemImplAtIndex(1)
      ->SetReferrer(web::Referrer(GURL(base::SysNSStringToUTF8(long_url)),
                                  web::ReferrerPolicy::ReferrerPolicyDefault));

  // Verify that storage has single item and that item does not have a referrer.
  SessionStorageBuilder builder;
  CRWSessionStorage* storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(1U, storage.itemStorages.count);

  EXPECT_EQ(GURL::EmptyGURL(), [storage.itemStorages.firstObject referrer].url);
}

// Tests building storage for session that has items longer than
// web::kMaxNavigationItemSize.
TEST_F(SessionStorageBuilderTest, SkipItemOverMaxSize) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(features::kReduceSessionSize);

  // Create WebState with navigation item count that are below maximum size.
  WebState::CreateParams params(GetBrowserState());
  WebStateWithMockProxy web_state(params);

  NSString* normal_url = @"https://foo.test";
  NSString* const current_url = normal_url;
  [web_state.fake_wk_list() setCurrentURL:normal_url
                             backListURLs:@[ normal_url ]
                          forwardListURLs:nil];
  OCMStub([web_state.mock_web_view() URL])
      .andReturn([NSURL URLWithString:current_url]);
  NavigationManager* navigation_manager = web_state.GetNavigationManager();
  ASSERT_EQ(2, navigation_manager->GetItemCount());

  SessionStorageBuilder builder;
  CRWSessionStorage* storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(2U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(web_state, 0, web::kMaxNavigationItemSize - 1,
                                0, 0, 0);
  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(2U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(web_state, 0, web::kMaxNavigationItemSize, 0, 0,
                                0);

  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(1U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(
      web_state, 0, web::kMaxNavigationItemSize - 1000 - 1, 1000 - 1, 0, 0);

  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(2U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(web_state, 0,
                                web::kMaxNavigationItemSize - 1000, 1000, 0, 0);

  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(1U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(web_state, 0,
                                web::kMaxNavigationItemSize - 1040 - 1,
                                1000 - 1, 40 - 1, 0);

  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(2U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(
      web_state, 0, web::kMaxNavigationItemSize - 1040, 1000, 40, 0);

  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(1U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(web_state, 0,
                                web::kMaxNavigationItemSize - 2040 - 1,
                                1000 - 1, 40 - 1, 1000 - 1);

  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(2U, storage.itemStorages.count);

  SetNavigationItemSizedStrings(
      web_state, 0, web::kMaxNavigationItemSize - 2040, 1000, 40, 1000);

  storage = builder.BuildStorage(&web_state);
  ASSERT_TRUE(storage);
  ASSERT_EQ(1U, storage.itemStorages.count);
}

}  // namespace web
